ci(release): split gen1/gen2 changesets and publish workflows - #6537
ci(release): split gen1/gen2 changesets and publish workflows#6537blunteshwar wants to merge 13 commits into
Conversation
Splits the shared publish pipeline into independent gen1 (main -> next/latest) and gen2 (gen2-beta -> beta, pre-release mode) release streams per SWC-2315, each with its own changeset folder so a version bump for one generation no longer forces a decision about the other. Introduces a fine-grained bot PAT (RELEASE_BOT_TOKEN, to be added as a repo secret + branch-protection bypass actor) for the version-bump push, replacing the default GITHUB_TOKEN. Adds a release-branch-lock composite action that temporarily blocks PR merges to the target branch for the duration of a real release run, closing the window where a concurrent merge could land between this job's checkout and its final push-back.
📚 Branch Preview Links🔍 Gen1 Visual Regression Test ResultsWhen a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:
Deployed to Azure Blob Storage: If the changes are expected, update the |
Coverage Report for CI Build 30333289161Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage remained the same at 96.236%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats💛 - Coveralls |
Moves from a root-level .changeset + .changeset-2nd-gen split (which needed a copy-into-.changeset shim at release time, since @changesets/cli only ever reads .changeset/ relative to wherever it's invoked) to two genuinely independent changesets instances: 1st-gen/.changeset/ and 2nd-gen/.changeset/, each with its own config.json, README, and now its own @changesets/cli devDependency so `yarn changeset` resolves correctly from within either directory. Removes the runtime copy-in/copy-out step from publish-2nd-gen.yml entirely - it's no longer needed. Also fixes every other repo-root reference to the old single .changeset/ path that would have silently broken or degraded (lint-staged, the 1st-gen changelog/test-changed scripts, the CircleCI docs-only-change filter, and two doc links), since 1st-gen's own nested `workspaces` field means changesets invoked there can no longer see out-of-scope packages by design - those ignore/fixed entries are dropped as dead weight rather than kept as unusable safety nets.
|
publish.yml no longer touches 2nd-gen at all, even for snapshot-release PRs - it never reads 2nd-gen/.changeset/ and has no 2nd-gen version/publish steps. publish-2nd-gen.yml gets its own pull_request (labeled/synchronize, gated to snapshot-release) trigger and handles its own snapshot testing independently: checks out the PR's own head ref, versions with --snapshot snapshot-test (never touching the persistent pre-release state), publishes under snapshot-test, and skips the branch lock and gen2-beta commit/push entirely since a PR run never touches gen2-beta.
Replace the read-whole-protection-object-then-PUT-it-back approach with GitHub's dedicated add/remove-contexts endpoints (POST|DELETE .../protection/required_status_checks/contexts). The prior PUT only round-tripped the fields this script knew about (required_status_checks, enforce_admins, required_pull_request_reviews, restrictions), so any other configured protection setting (allow_force_pushes, required_linear_history, etc.) would silently reset to its API default on every lock and unlock. Also renames action.yaml -> action.yml to match the repo's predominant action/workflow file extension.
Addressed all three:
|
Architecture researchBefore this PRSingle Constraints this created:
Decisions made
Changeset placementFully independent instances —
Verified locally: Current shape
Known follow-up (not done here)
|
@main doesn't have .github/actions/release-branch-lock yet - it only exists on this unmerged branch, so every pre-checkout lock step hard-failed with "Can't find action.yml" (seen in run 30274425726). Pin to ${{ github.sha }} instead, which resolves the action from whatever commit is actually running: works now while testing on this branch, and continues to work after merge.
uses: doesn't support any expression context, including github - so pinning
to ${{ github.sha }} was invalid syntax, not just a wrong ref (confirmed by
the workflow validator: "Unrecognized named-value: 'github'"). And a literal
@main reference 404s since this action doesn't exist on main yet.
Drop the composite-action self-reference for the pre-checkout lock step
entirely and inline the gh api call directly - a raw run: step needs no
uses: and no repo checkout, so there's no ref to get wrong. Post-checkout
steps (unlock in both workflows, both lock/unlock in release-branch-unlock.yml)
keep using the composite action via its local ./path reference, which is fine
once checkout has happened.
@changesets/changelog-github (2nd-gen's changelog generator, same as 1st-gen's) needs GITHUB_TOKEN to fetch PR/commit info for changelog entries. publish.yml's equivalent step already sets it; publish-2nd-gen.yml's "Version packages" step didn't, so `yarn changeset version` would fail with "Please create a GitHub personal access token ... and add it as the GITHUB_TOKEN environment variable" - reproduced locally before this fix, confirmed resolved after (2.0.0-beta.2 -> 2.0.0-beta.3, CHANGELOG.md and package.json updated correctly for both 2nd-gen packages).
The Verify NPM authentication step wrote ~/.npmrc, but setup-node sets
NPM_CONFIG_USERCONFIG so npm reads its own temp .npmrc instead, which
authenticates through ${NODE_AUTH_TOKEN}. That token was never set here,
so npm whoami got the placeholder and returned 401. Set NODE_AUTH_TOKEN to
ADOBE_BOT_NPM_TOKEN and drop the no-op ~/.npmrc write.
2nd-gen now releases from main on its own cadence (still beta tag, changesets pre-release mode). Retarget every gen2-beta reference to main: push trigger, branch lock/unlock, checkout ref, commit/push target, the changeset baseBranch, the force-unlock branch choice, and the changeset README. No gen2-beta branch is involved anymore.
2nd-gen now publishes two dist-tags with different mechanics: - push to main -> next: continuous throwaway snapshot (changeset version --snapshot next, publish --tag next). No pre-release mode, no branch lock, nothing committed back to main - a rolling build of main for consumers (e.g. the Photoshop dry-run PR) to pull the latest changes. - workflow_dispatch (main only) -> beta: planned pre-release. Enters changesets pre-release mode (beta.N), locks main, versions + changelog, commits the bump back to main. - pull_request + snapshot-release label -> snapshot-test (unchanged). A manual dispatch is restricted to main (github.ref guard); off-main dispatches are skipped. latest is intentionally not supported yet.
Architecture research
Before this PR
Single
publish.yml, single root.changeset/. Oneyarn changeset versionrun bumped every pending changeset regardless of generation; oneyarn changeset publishshipped everything under one tag; final step committedversion bumps and pushed straight to
mainwith the defaultGITHUB_TOKEN.Constraints this created:
betatag from its own branch.mainneeded branch-protection to either not exist or allow Actions to bypass it.mainonce, publishes to npm against that snapshot, then rebases+pushes at the very end. A concurrent merge in that window could conflict the push — after npm publish already happened, so git andnpm state could diverge.
Decisions made
RELEASE_BOT_TOKEN), added as a branch-protection bypass actor — not thechangesets/actionversion-bump-PR pattern. Keeps the existing commit-and-push shape.release-branch-lockcomposite action adds a synthetic required status check (release/in-progress) to the target branch for the release'sduration, blocking PR merges into it until unlocked. Required status checks only gate the merge button/API, not a direct
git push, so it doesn't interfere with the release job's own push.Changeset placement
Fully independent instances —
1st-gen/.changeset/and2nd-gen/.changeset/, no shared folder, each with its ownconfig.json/README.md. Two findings while building this:@changesets/clihas no--cwdflag (checked--helpand the bundled source). The real mechanism is invoking it from inside the target directory (working-directory:in CI,cdlocally) — it walks up to find themonorepo root for package resolution, but reads config/writes changesets relative to the invocation dir.
1st-gen/package.jsonalready declares its own nestedworkspacesfield. Running changesets from inside it means package resolution never sees 2nd-gen's packages at all — they're structurally invisible, not justconfig-ignored. Had to strip dead
ignore/fixedentries referencing out-of-scope packages, and add@changesets/cli+@changesets/changelog-githubto2nd-gen/package.json's owndevDependencies(it had none — only rootand
1st-gendid).Verified locally:
yarn changeset statusfrom each directory resolves correctly against its own config and packages.Current shape
publish.yml— 1st-gen only, every trigger, including its ownsnapshot-releasePR path. Never touches2nd-gen/.changeset/.publish-2nd-gen.yml— fully independent counterpart: ownpull_requesttrigger pluspush-to-main(autonext) andworkflow_dispatch(plannedbeta) triggers; see the trigger table below. This overrides SWC-2315's original "one workflow snapshots bothgens" row — total independence was an explicit requirement, including for snapshots.
.changeset/reference that would've silently broken:lint-staged.config.js, three1st-gen/scripts/*.jschangelog/test scripts,.circleci/config.yml's docs-only-change filter, two doc links.Known follow-up (not done here)
scripts/publish.js(standalone local "unified" publish script) still assumes a root.changeset/that no longer exists — not wired into CI, but broken if run locally. Needs a decision (split it or retire it), trackedseparately.
Description
MVP for a multi-stream release architecture: splits the single shared publish pipeline into two fully independent release streams, one per generation.
1st-gen/.changeset/and2nd-gen/.changeset/are two genuinely independent@changesets/cliinstances (ownconfig.json, ownREADME.md, own pending changeset files) — not a shared root folder.@changesets/clihas no--cwdflag; each instance works by being invoked from inside its own directory (working-directory:in CI,cdlocally).yarn changeset:1st-gen/yarn changeset:2nd-genare thin wrappers for that.publish.ymlis 1st-gen only, in every trigger, including its ownsnapshot-releasePR path — it never reads2nd-gen/.changeset/and never runs a 2nd-gen step.publish-2nd-gen.ymlis the fully independent counterpart: the fully independent counterpart, with three release streams (see the trigger table below):pushtomainauto-publishes a throwawaynextsnapshot,workflow_dispatch(frommainonly) cuts the plannedbetapre-release, and asnapshot-releasePR publishessnapshot-test. There is no separategen2-betabranch; 2nd-gen releases frommain, and its own dist-tags keep it distinct from 1st-gen'snext/latest.release-branch-lockcomposite action adds a synthetic required status check to the target branch for the duration of a real release, blocking PR merges into it until the release unlocks it. The lock step runs before checkout (via the action's full remote ref, since a local./pathreference needs the repo already checked out to resolve), so the race window is closed from the start of the job, not just narrowed after checkout + install.release-branch-unlock.yml: manualworkflow_dispatchrecovery — if a release run is cancelled or the runner dies,if: always()doesn't fire and the lock is stuck; this clears it by hand.RELEASE_BOT_TOKEN) instead of the defaultGITHUB_TOKEN.npm whoami) before gating the publish step on it, rather than a step that always reports success.Gen2 release triggers
publish-2nd-gen.ymlpublishes two dist-tags with different mechanics:mainmainpushtomainnext--snapshot(throwaway)workflow_dispatch(frommainonly)betabeta.N)pull_request+snapshot-releasesnapshot-test--snapshot(throwaway)nextis a continuous, throwaway snapshot ofmainpublished on every merge, so consumers (e.g. the always-open Photoshop dry-run PR) can always pull the latestmainbuild.betais the planned, manually-cut release with a changelog and announcement.latestis intentionally not supported for 2nd-gen yet. A manual dispatch is restricted tomain(a dispatch against any other ref is skipped).Motivation and context
Gen1 and Gen2 are expected to diverge in release cadence, validation, and promotion strategy, but previously shared one changeset folder and one workflow — every real release run processed both gens together, forced a shared version decision, and pushed back to
maindirectly with no protection against a concurrent merge landing mid-run. This is the SWC-2282 assessment's recommended fix, built out to the SWC-2315 acceptance criteria as a draft MVP — with one explicit deviation: total independence extends to snapshot releases too, sopublish.ymlnever touches 2nd-gen even forsnapshot-releasePR testing (SWC-2315's original acceptance table had one workflow snapshot both gens; this PR does not implement that row).Full architecture write-up, constraints, and the design decisions behind this shape live in
research.mdat the repo root (gitignored, not part of the diff — condensed version posted as a PR comment below).Related issue(s)
Manual setup required before this can run for real (cannot be done from a PR)
Contents: read & writeandAdministration: read & writeon this repo; add it as repo secretRELEASE_BOT_TOKENin thenpm-publishenvironment.main's branch protection so it can push directly.Screenshots (if appropriate)
N/A — CI/release infrastructure only, no UI change.
Author's checklist
1st-gen/.changeset/README.md,2nd-gen/.changeset/README.md,CONTRIBUTOR-DOCSpath fixes).Reviewer's checklist
Manual review test cases
1st-gen changeset releases correctly, independent of 2nd-gen
publish.ymlviaworkflow_dispatchwithtag=nextagainst a branch with only1st-gen/.changeset/*.mdpresent2nd-gen/.changeset/never read, never touchedGen2 dry run reports without side effects
publish-2nd-gen.ymlviaworkflow_dispatchwithdry_run=truemainunchanged (no commit/push, no lock taken)Each gen's snapshot-release PR path is independent
snapshot-releasewhile both1st-gen/.changeset/and2nd-gen/.changeset/have pending filespublish.ymlsnapshots 1st-gen only undersnapshot-test;publish-2nd-gen.ymlsnapshots 2nd-gen only undersnapshot-test, checked out from the PR's own head ref. Neither workflow reads the other gen's folder; neither commits/pushes on this path.Branch lock closes the race window from before checkout
tag=latestrun onpublish.yml(or abetaworkflow_dispatchrun on the 2nd-gen workflow) against a protected branchrelease/in-progressrequired status check appears on the branch as the very first step (before checkout completes), stays for the run, and is removed once the job finishes, even if a later step failsStuck-lock recovery
release/in-progressremains on the branch; runningrelease-branch-unlock.ymlwith that branch selected removes itDevice review
Accessibility testing checklist
N/A — this PR only changes CI/release workflow configuration; no component, page, or interactive surface is affected. No keyboard or screen reader testing applies.